﻿using System.Diagnostics.Contracts;
using System.Reflection;

namespace System.ComponentModel
{
	internal sealed class PropertyDescriptor : MemberDescriptor
	{
		#region Public Properties
		public override string Name
		{
			get
			{
				var name = property.Name;

				Contract.Assume(!string.IsNullOrEmpty(name));

				return name;
			}
		}

		public Type PropertyType
		{
			get
			{
				Contract.Ensures(Contract.Result<Type>() != null);

				return property.PropertyType;
			}
		}

		public bool IsReadOnly
		{
			get
			{
				return !property.CanWrite;
			}
		}

		public bool SupportsChangeEvents
		{
			get
			{
				return EnsureChangeEvent();
			}
		}
		#endregion

		#region Private / Protected
		private readonly PropertyInfo property;
		private EventInfo changeEvent;
		private bool changeEventInitialized;
		#endregion

		#region Constructors
		internal PropertyDescriptor(PropertyInfo property)
		{
			Contract.Requires(property != null);

			this.property = property;
		}
		#endregion

		#region Methods
		[ContractInvariantMethod]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")]
		private void ObjectInvariant()
		{
			Contract.Invariant(property != null);
		}

		public object GetValue(object source)
		{
			return property.GetValue(source, null);
		}

		public void SetValue(object source, object value)
		{
			property.SetValue(source, value, null);
		}

		public void AddValueChanged(object source, EventHandler handler)
		{
			if (!EnsureChangeEvent())
			{
				throw new InvalidOperationException();
			}

			changeEvent.AddEventHandler(source, handler);
		}

		public void RemoveValueChanged(object source, EventHandler handler)
		{
			if (!EnsureChangeEvent())
			{
				throw new InvalidOperationException();
			}

			changeEvent.RemoveEventHandler(source, handler);
		}

		private bool EnsureChangeEvent()
		{
			Contract.Ensures(Contract.Result<bool>() == (changeEvent != null));

			if (!changeEventInitialized)
			{
				var type = property.DeclaringType;

				Contract.Assume(type != null);

				bool isNotifier = typeof(INotifyPropertyChanged).IsAssignableFrom(type);

				if (isNotifier)
				{
					var map = type.GetInterfaceMap(typeof(INotifyPropertyChanged));

					Contract.Assume(map.TargetMethods != null);
					Contract.Assume(map.TargetMethods.Length > 0);

					var propertyChangedAddOrRemove = map.TargetMethods[0];

					foreach (var eventInfo in type.GetEvents(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
					{
						Contract.Assume(eventInfo != null);

						if (propertyChangedAddOrRemove == eventInfo.GetAddMethod(nonPublic: true)
							|| propertyChangedAddOrRemove == eventInfo.GetRemoveMethod(nonPublic: true))
						{
							changeEvent = eventInfo;
							break;
						}
					}
				}
				else
				{
					changeEvent = type.GetEvent(property.Name + "Changed", TypeDescriptor.DefaultBindings);
				}

				changeEventInitialized = true;
			}

			return changeEvent != null;
		}
		#endregion
	}
}
